﻿//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: Tokenizer.cs
//
//  Description: Extracts tokens from a string.
// 
//--------------------------------------------------------------------------

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

namespace Chili.Opf3.Query.OPathParser
{
	/// <summary>
	/// Returns tokens of the OPath expression.
	/// </summary>
	/// <remarks>
	/// The tokenizer parses the OPath expression and returns tokens that are processed
	/// by the <see cref="OPathParser">OPathParser</see> to create a valid 
	/// <see cref="Opf3.Query.ObjectExpression">ObjectExpression</see>.
	/// </remarks>
	internal sealed class Tokenizer : IEnumerable<Token>, IEnumerator<Token>, IEnumerable, IEnumerator
	{
		private char[] _value;
		private Token _current;
		private const char _eof = '\0';
		private int _column;
		private int _lastTokenColumn;

		/// <summary>
		/// Creates a new instance of the <see cref="Tokenizer">Tokenizer Class</see>.
		/// </summary>
		/// <param name="value">Value that is processed by the <see cref="Tokenizer">Tokenizer</see>.</param>
		public Tokenizer(string value)
		{
			_value = value.ToCharArray();
		}

		#region IEnumerable<T> Members

		/// <summary>
		/// Returns an enumerator that iterates through this <see cref="Tokenizer">Tokenizer</see>.
		/// </summary>
		public IEnumerator<Token> GetEnumerator()
		{
			return this;
		}

		#endregion

		#region IEnumerator<T> Members

		/// <summary>
		/// Returns the current <see cref="Token">Token</see> processed by the <see cref="Tokenizer">Tokenizer</see>.
		/// </summary>
		/// <value>
		/// The current <see cref="Token">Token</see> processed by the <see cref="Tokenizer">Tokenizer</see>.
		/// </value>
		public Token Current
		{
			get
			{
				// If no valid token has been selected throw an exception.
				if (_current == null)
					throw new InvalidOperationException("Tokenizer is not on a valid item.");

				return _current;
			}
		}

		/// <summary>
		/// Moves the <see cref="Tokenizer">Tokenizer</see> to the next item.
		/// </summary>
		/// <returns>
		/// Returns true if it was possible to move the the next item.
		/// </returns>
		public bool MoveNext()
		{
			_current = Next();
			return (_current != null);
		}

		/// <summary>
		/// Resets the <see cref="Tokenizer">Tokenizer</see>. This method is not supported, 
		/// since the <see cref="Tokenizer">Tokenizer</see> is forward-only.
		/// </summary>
		/// <exception cref="NotImplementedException">Exception is thrown since the method is not supported.</exception>
		public void Reset()
		{
			throw new NotImplementedException("Tokenizer is forward-only! Reset is not supported.");
		}

		#endregion

		#region IEnumerator Members

		/// <summary>
		/// Returns the current <see cref="Token">Token</see> processed by the <see cref="Tokenizer">Tokenizer</see>.
		/// </summary>
		/// <value>
		/// The current <see cref="Token">Token</see> processed by the <see cref="Tokenizer">Tokenizer</see>.
		/// </value>
		object IEnumerator.Current
		{
			get { return ((IEnumerator<Token>)this).Current; }
		}

		#endregion

		#region IEnumerable Members

		/// <summary>
		/// Returns an enumerator that iterates through this <see cref="Tokenizer">Tokenizer</see>.
		/// </summary>
		IEnumerator IEnumerable.GetEnumerator()
		{
			return this;
		}

		#endregion

		#region IDisposable Members

		/// <summary>
		/// Disposes the <see cref="Tokenizer">Tokenizer</see>.
		/// </summary>
		public void Dispose()
		{
			// Nothing to do here.
		}

		#endregion

		/// <summary>
		/// Moves the <see cref="Tokenizer">Tokenizer</see> to the next item.
		/// </summary>
		/// <returns>
		/// Returns null if it was possible to move the the next item.
		/// </returns>
		private Token Next()
		{
			// Reads the first char from the value and returns that value.
			char c = Read();
			bool cont = true;

			while (cont)
			{
				cont = false;
				switch (c)
				{
					// We have finished, return null and leave.
					case _eof:
						return null;

					// This characters are not important for our engine. Leave them and proceed with the
					// next one.
					case ' ':
					case '\t':
					case '\n':
					case '\r':
						_lastTokenColumn = _column;
						c = Read();
						cont = true;
						break;

					case '(':
						return CreateToken(TokenKinds.OpenParenthesis);

					case ')':
						return CreateToken(TokenKinds.CloseParenthesis);

					case '=':
					case '<':
					case '>':
						return ReadComparer();

					case '{':
						return ReadParameter();

					default:
						// If the char is a letter we know that it must be a Property or a keyword.
						if (Char.IsLetter(c))
							return ReadWord();
						else
						{
							// Check if the last token is a comprarer. The next token is then a parameter.
							if (_current != null && _current.TokenKind == TokenKinds.Comparer)
								return ReadInlineParameter(c);
							else
								return CreateToken(TokenKinds.Unknown);
						}
				}
			}

			// Throw an exception here. Should never happen.
			throw new OPathException("Critical exception encountered.", string.Empty, _value.ToString());
		}

		/// <summary>
		/// Reads the next char in the tokenizer and advances the tokenizer.
		/// </summary>
		/// <returns>Returns the char that has been read or EOF if the string is finished.</returns>
		private char Read()
		{
			char c = ReadAhead();
			if (c != _eof)
				_column++;
			return c;
		}

		/// <summary>
		/// Reads the next char in the tokenizer. Does not advance the tokenizer.
		/// </summary>
		/// <returns>Returns the char that has been read or EOF if the string is finished.</returns>
		private char ReadAhead()
		{
			// Read the next char.
			if (_column + 1 <= _value.Length)
				return _value[_column];
			else
			{
				// Nothing left to read.
				return _eof;
			}
		}

		/// <summary>
		/// Creates a new <see cref="Token">Token</see> of the given <see cref="TokenKinds">TokenKind</see>.
		/// </summary>
		/// <param name="tokenKind">The type of the <see cref="Token">Token</see> that is generated.</param>
		/// <returns>A <see cref="Token">Token</see> with the given type.</returns>
		private Token CreateToken(TokenKinds tokenKind)
		{
			Token token = new Token(tokenKind, GetCurrentTokenValue());
			// Set the current column as last token column.
			_lastTokenColumn = _column;
			return token;
		}

		/// <summary>
		/// Gets the value for the token that is generated.
		/// </summary>
		private string GetCurrentTokenValue()
		{
			return (_column - _lastTokenColumn) > 0 ?
				new string(_value, _lastTokenColumn, (_column - _lastTokenColumn)).Trim() : null;
		}

		/// <summary>
		/// Reads the whole next word.
		/// </summary>
		/// <returns>Returns a <see cref="Token">Token</see> that contains the next word.</returns>
		private Token ReadWord()
		{
			char c = ReadAhead();
			// Read until we get something that is not part of a word.
			while (char.IsLetterOrDigit(c) || c == '_' || c == '.')
			{
				Read();
				c = ReadAhead();
			}

			// Check the type of word we encountered.
			switch (c)
			{
				case '(':
					return ReadCommand();

				case '[':
					return ReadJoin();

				default:
					{
						// Check the value of the token. If it is a keyword create a special token.
						string value = GetCurrentTokenValue().ToLower();
						switch (value)
						{
							case "and":
								return ReadConcat();
							case "or":
								return ReadConcat();
							case "sortby":
								return ReadSortBy();
							case "like":
								return ReadComparer();
							case "is":
								return ReadComparer();
							case "isnot":
								return ReadComparer();
							case "null":
								{
									// Null is a token that is used as parameter to compare a field value with DBNull.Value.
									return CreateToken(TokenKinds.Parameter);
								}
							default:
								return CreateToken(TokenKinds.Member);
						}
					}
			}
		}

		/// <summary>
		/// Reads the whole next command.
		/// </summary>
		/// <returns>Returns a <see cref="Token">Token</see> that contains the next command.</returns>
		private Token ReadCommand()
		{
			// Holds the amount of open brakets.
			int openBrakets = 0;
			// True if there is an open quota.
			bool openDoubleQuotas = false;
			bool openSingleQuotas = false;

			while (true)
			{
				// Read the next char.
				char c = Read();

				// Read the whole command. Check also for quotas. Only outside of quotas is read.
				switch (c)
				{
					case '(':
						if (!openSingleQuotas && !openDoubleQuotas)
							openBrakets++;
						break;

					case ')':
						if (!openSingleQuotas && !openDoubleQuotas)
							openBrakets--;
						if (openBrakets == 0)
							return CreateToken(TokenKinds.Command);
						break;

					case '\'':
						if (!openDoubleQuotas)
							openSingleQuotas = !openSingleQuotas;
						break;

					case '\"':
						if (!openSingleQuotas)
							openDoubleQuotas = !openDoubleQuotas;
						break;

					case _eof:
						throw new OPathException("Command has not been terminated. Missing closing ')' or misplaced quotas?",
							string.Empty, _value.ToString());
				}
			}
		}

		/// <summary>
		/// Reads the whole next joing.
		/// </summary>
		/// <returns>Returns a <see cref="Token">Token</see> that contains the next join.</returns>
		private Token ReadJoin()
		{
			// Holds the amount of open brakets.
			int openBrakets = 0;
			// True if there is an open quota.
			bool openDoubleQuotas = false;
			bool openSingleQuotas = false;

			while (true)
			{
				// Read the next char.
				char c = Read();

				// Read as long we are encounting the finishing ] and we are not inside quotas.
				switch (c)
				{
					case '[':
						if (!openDoubleQuotas && !openSingleQuotas)
							openBrakets++;
						break;

					case ']':
						if (!openDoubleQuotas && !openSingleQuotas)
							openBrakets--;
						if (openBrakets == 0)
							return CreateToken(TokenKinds.Join);
						break;

					case '\'':
						if (!openDoubleQuotas)
							openSingleQuotas = !openSingleQuotas;
						break;

					case '\"':
						if (!openSingleQuotas)
							openDoubleQuotas = !openDoubleQuotas;
						break;

					case _eof:
						throw new OPathException("Invalid join encountered. Missing closing ']'?", string.Empty,
							_value.ToString());
				}
			}
		}

		/// <summary>
		/// Reads the whole next concat.
		/// </summary>
		/// <returns>Returns a <see cref="Token">Token</see> that contains the next concat.</returns>
		private Token ReadConcat()
		{
			return CreateToken(TokenKinds.Concat);
		}

		/// <summary>
		/// Reads the whole next concat.
		/// </summary>
		/// <returns>Returns a <see cref="Token">Token</see> that contains the next concat.</returns>
		private Token ReadSortBy()
		{
			char c = Read();
			// SortBy is always the last one. We read to the end.
			while (c != _eof)
			{
				c = Read();
			}

			return CreateToken(TokenKinds.SortBy);
		}

		/// <summary>
		/// Reads the whole next comparer.
		/// </summary>
		/// <returns>Returns a <see cref="Token">Token</see> that contains the next comparer.</returns>
		private Token ReadComparer()
		{
			// Read the whole comparer.
			char c = ReadAhead();
			if (c == '=' || c == ' ' || c == '>')
				Read();

			return CreateToken(TokenKinds.Comparer);
		}

		/// <summary>
		/// Reads the whole next parameter.
		/// </summary>
		/// <returns>Returns a <see cref="Token">Token</see> that contains the next parameter.</returns>
		private Token ReadParameter()
		{
			// Read the parameter replacement. The number that represents the parameter: for example {0}.
			char c = Read();
			while (c != '}')
			{
				if (c == _eof)
					throw new OPathException("Invalid parameter encountered. Missing closing '}'?", string.Empty,
						_value.ToString());

				c = Read();
			}
			return CreateToken(TokenKinds.Parameter);
		}

		/// <summary>
		/// Reads the whole next inline parameter.
		/// </summary>
		/// <returns>Returns a <see cref="Token">Token</see> that contains the next inline parameter.</returns>
		private Token ReadInlineParameter(char c)
		{
			// Read the parameter if they are inlined in the query.
			bool openSingleQuotas = false;
			bool openDoubleQuotas = false;

			while (true)
			{
				switch (c)
				{
					case '\'':
						if (!openDoubleQuotas)
						{
							openSingleQuotas = !openSingleQuotas;
							// If the quotas have been closed go to default.
							if (!openSingleQuotas)
								goto default;
						}
						break;

					case '\"':
						if (!openSingleQuotas)
						{
							openDoubleQuotas = !openDoubleQuotas;
							// If the quotas have been closed go to default.
							if (!openDoubleQuotas)
								goto default;
						}
						break;

					case _eof:
						if (openDoubleQuotas || openSingleQuotas)
							throw new OPathException("Invalid parameter encountered. Missing closing quotas?",
								string.Empty, _value.ToString());
						else
							goto default;

					default:

						// Read the next char.
						c = ReadAhead();

						// Only create the token if the word has finished or we are outside the quotas.
						if (!char.IsLetterOrDigit(c) && !openSingleQuotas && !openDoubleQuotas)
							return CreateToken(TokenKinds.Parameter);
						break;
				}

				// Read the next char.
				c = Read();
			}
		}
	}
}
